/*
 * Copyright 2003 Solers Corporation.  All rights reserved.
 *
 * Modification and use of this SLP API software and
 * associated documentation ("Software") is permitted provided that the
 * conditions specified in the license.txt file included within this
 * distribution are met.
 *
 */

package com.solers.slp;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.StringTokenizer;
import java.util.Vector;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Implements the ServiceLocationEnumeration interface defined in RFC 2614.
 * 
 * @author Patrick Callis
 */
class ServiceLocationEnumerationImpl implements ServiceLocationEnumeration {

	/** Default Logging */
	private Logger mLogger;

	private SLPConfiguration _conf;

	private NetworkManager _net;
	private UAMessage _message;

	/** Used for writing UA messages */
	private ByteArrayOutputStream uaBos;
	/** Wrapper for ByteArrayOutputStream */
	private DataOutputStream uaDos;
	/** Datagram socket for UA messages */
	private DatagramSocket _uaSock;

	/** Array of timeout values read from configuration */
	private int[] _retry;
	/** Max time to wait for response to UA message */
	private int _maxWait;

	private Vector _received = new Vector();
	private Vector _delivered = new Vector();

	/**
	 * Used to remember if we've asked for responses yet. If this is a unicast
	 * request, don't wait for more than one response.
	 */
	private boolean _requested = false;

	/** Holds the errorCode from the last response */
	private short _errorCode = 0;

	ServiceLocationEnumerationImpl(NetworkManager net, UAMessage message)
			throws ServiceLocationException {
		_conf = ServiceLocationManager.getConfiguration();
		mLogger = Logger.getLogger(ServiceLocationManager.getLoggerName());

		_retry = _conf.getMcastTimeouts();
		_maxWait = _conf.getMcastMaxWait();

		_net = net;
		_message = message;

		uaBos = new ByteArrayOutputStream(_conf.getMTU());
		uaDos = new DataOutputStream(uaBos);

		_message.setXid(_net.nextXid());

		try {
			_uaSock = new DatagramSocket();
			transmitDatagram();
		} catch (IOException e) {
			throw new ServiceLocationException("Could not send message.",
					ServiceLocationException.NETWORK_ERROR);
		}
	}

	/**
	 * Returns the next SLP response received.
	 * 
	 * @exception com.solers.slp.ServiceLocationException
	 *                if an error is received.
	 * @exception java.util.NoSuchElementException
	 *                if no more responses.
	 */
	@SuppressWarnings("unchecked")
	public Object next() throws ServiceLocationException {
		try {
			if (_received.size() == 0 && (_message.isMcast() || !_requested)
					&& _errorCode == 0) {
				_errorCode = readResponse();
				_requested = true;
			}

			if (_errorCode != 0) {
				short error = _errorCode;
				_errorCode = 0;
				throw new ServiceLocationException("Error from SA", error);
			}

			if (_received.size() > 0) {
				Object o = _received.remove(0);
				_delivered.add(o);
				return o;
			} else {
				throw new NoSuchElementException();
			}
		} catch (IOException e) {
			mLogger.log(Level.SEVERE, "Exception reading responses.", e);
			throw new ServiceLocationException(e.toString(),
					ServiceLocationException.NETWORK_ERROR);
		}
	}

	/**
	 * Returns whether more responses exist. In order to allow clients to only
	 * block for responses they want, this method must try to receive more
	 * responses if none are queued.
	 */
	public boolean hasMoreElements() {
		if (_received.size() > 0) {
			return true;
		} else if (_message.isMcast() || !_requested) {
			try {
				_errorCode = readResponse();
				_requested = true;
			} catch (EOFException eof) {
				// EOF is probably OK, so just log it as info
				mLogger.log(Level.INFO,
						"Encountered End of File, this is probably OK", eof);
			} catch (IOException e) {
				mLogger.log(Level.SEVERE, "Exception reading responses.", e);
			}
		}

		return (_received.size() > 0);
	}

	/**
	 * @see #next()
	 */
	public Object nextElement() {
		try {
			return next();
		} catch (ServiceLocationException e) {
			throw new NoSuchElementException(e.toString());
		}
	}

	/**
	 * Performs the actual transmission of a UA message.
	 */
	private void transmitDatagram() throws IOException {
		InetAddress addr = _net.getDaAddress();

		if (_conf.getTraceMessage()) {
			mLogger.info("Sending: " + _message.toString());
		}

		uaBos.reset();
		if (!_message.writeMessage(uaDos)) {
			// Message doesn't fit in MTU,
			// probably too many previous responders
			return;
		}
		byte[] buf = uaBos.toByteArray();

		if (addr.isMulticastAddress()) {
			_message.setMcast(true);
			_message.setTransmitSchedule(_conf.getMcastTimeouts());
			buf[5] = (byte) (buf[5] | 0x20);
		} else {
			_message.setMcast(false);
			_message.setTransmitSchedule(_conf.getDatagramTimeouts());
			buf[5] = (byte) (buf[5] & 0xDF);

			if (_conf.getTraceDaTraffic()) {
				mLogger.info("Sending message to DA with XID "
						+ _message.getXid());
			}
		}

		buf[10] = (byte) (_message.getXid() >> 8);
		buf[11] = (byte) (_message.getXid() & 0xFF);
		DatagramPacket p = new DatagramPacket(buf, buf.length);

		p.setAddress(addr);
		p.setPort(NetworkManager.SLP_RESERVED_PORT);
		_uaSock.send(p);
		_message.sent();
	}

	/**
	 * Listens for a response to a UA message. The message may be retransmitted
	 * if necessary. This method implements the multicast convergence algorithm
	 * described in RFC 2608.
	 */
	short readResponse() throws IOException {
		byte[] buf = new byte[_conf.getMTU()];
		DatagramPacket p = new DatagramPacket(buf, _conf.getMTU());
		long started = System.currentTimeMillis();
		int failCount = 0;
		short errorCode = 0;
		boolean gotResponse = false;

		while (_received.size() == 0 // Received something
				&& (_message.isMcast() || !gotResponse) // DA returned no
														// results
				&& started + _maxWait > System.currentTimeMillis() // Request
																	// timeout
				&& failCount < 2) { // Two consecutive non-responses
			try {
				_uaSock.setSoTimeout(_message.nextTimeout());
				_uaSock.receive(p);

				// Check xid
				short xid = (short) ((p.getData()[10] << 8) | (p.getData()[11] & 0xff));

				if (xid == _message.getXid()) {
					if (_conf.getTraceDaTraffic() && !_message.isMcast()) {
						mLogger.info("Received response from DA with XID "
								+ xid);
					}
					failCount = 0;
					gotResponse = true;
					_message.addResponder(p.getAddress().getHostAddress());
					errorCode = parseResponse(p.getData());
					removeDups();
				} else {
					mLogger.warning("Dropping message with XID " + xid);
				}
			} catch (InterruptedIOException e) {
				failCount++;
				transmitDatagram();
			}
		}

		if (!gotResponse && !_message.isMcast()) {
			// If no response from DA, it may be down.
			_net.findDA();
			return readResponse();
		}

		return errorCode;
	}

	/**
	 * Removes duplicate entries from the list of responses.
	 */
	private void removeDups() {
		Iterator iter = _delivered.iterator();
		while (iter.hasNext()) {
			Object o;
			if (_received.contains(o = iter.next())) {
				_received.remove(o);
			}
		}
	}

	/**
	 * Parses a datagram packet's buffer, adding responses to the list of
	 * received values.
	 */
	@SuppressWarnings({ "unchecked", "rawtypes" })
	private short parseResponse(byte[] buf) throws IOException {

		if (buf == null) {
			return 0;
		}

		ByteArrayInputStream bis = new ByteArrayInputStream(buf);
		DataInputStream dis = new DataInputStream(bis);

		dis.readByte();
		int functionID = dis.readByte();
		dis.skipBytes(3);
		byte flags = dis.readByte();
		boolean overflow = false;
		if ((flags & 0x80) != 0) {
			overflow = true;
		}

		dis.skipBytes(6);
		int len = dis.readShort() & 0xFFFF;
		dis.skipBytes(len);

		short errorCode = dis.readShort();

		int size;

		switch (functionID) {
		case SLPMessage.DAADVERT:
			// DA Advertisement
			dis.skipBytes(4);
			size = dis.readShort() & 0xFFFF;
			byte[] host = new byte[size];
			dis.read(host);
			_received.add(new String(host));
			break;

		case SLPMessage.SRVRPLY:
			// Service Reply
			size = dis.readShort() & 0xFFFF;
			for (int i = 0; i < size; i++) {
				ServiceURL result = new ServiceURL();
				try {
					result.readExternal(dis);
					if (result.verify()) {
						_received.add(result);
					}
				} catch (EOFException e) {
					if (overflow) {
						break;
					} else {
						throw e;
					}
				}
			}
			break;

		case SLPMessage.ATTRRPLY:
			// Attribute Reply
			size = dis.readShort() & 0xFFFF;
			byte[] attrs = new byte[size];
			dis.read(attrs);
			String list = new String(attrs);
			AuthenticationBlock[] authBlocks = new AuthenticationBlock[dis
					.readByte()];
			for (int j = 0; j < authBlocks.length; j++) {
				authBlocks[j] = new AuthenticationBlock();
				authBlocks[j].readBlock(dis);
			}
			if (ServiceLocationAttribute.verifyList(list, authBlocks)) {
				Vector v = ServiceLocationAttribute.readFromList(list);
				Iterator i = v.iterator();
				while (i.hasNext()) {
					ServiceLocationAttribute sla = (ServiceLocationAttribute) i
							.next();
					_received.add(sla);
				}
			}
			break;

		case SLPMessage.SRVTYPERPLY:
			// Service Type Reply
			size = dis.readShort() & 0xFFFF;
			byte[] types = new byte[size];
			dis.read(types);
			StringTokenizer st = new StringTokenizer(new String(types), ",");
			while (st.hasMoreElements()) {
				_received.add(new ServiceType(st.nextToken()));
			}
			break;
		}
		return errorCode;
	}

	/**
	 * Frees resources allocated by this enumeration.
	 */
	public void destroy() {
		if (uaBos != null) {
			try {
				uaBos.close();
			} catch (IOException e) {
			}
			uaBos = null;
		}
		if (_uaSock != null) {
			_uaSock.close();
			_uaSock = null;
		}

	}
}
